package qy.qyalgotrader.view.swing;

import com.google.common.collect.BiMap;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.Table;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.csv.CSVRecord;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import qy.jalgotrade.bar.Bar;
import qy.jalgotrade.utils.CommonUtils;
import qy.qyalgotrader.mystrategy.Inspectable;
import qy.qyalgotrader.utils.CommonDataUtils;
import qy.qyalgotrader.utils.Constants;

import javax.swing.*;
import javax.swing.table.*;
import java.awt.*;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.text.Format;
import java.text.NumberFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.*;
import java.util.List;

/**
 *
 */
public class SwingPullView {

	private static final Logger __log = LoggerFactory.getLogger(SwingPullView.class);

	/**
	 *
	 */
	private class IndicatorsTableModel extends AbstractTableModel {

		private final List<String> __headerNames;

		private final List<String> __constColumns;

		private final List<String> __recKeys;

		private final Pair<String, Integer> __seqColumn;

		/**
		 * <pre>
		 * 展现的 指标名称 -> 列坐标 的映射; 例: {"RT_OPEN": "今开", "AAR_T1": "AAR_T1", ...};
		 * key: indicatorName;
		 * value: TableColumnName.
		 * </pre>
		 */
		private final BiMap<String, String> __indicatorNameBiMap;

		/**
		 * 常量列, 例如: WindCode, 名称, 状态, ...
		 */
		private final Table<String, String, Object> __constants;

		/**
		 * 日期 (Constants.DATE), 时间 (Constants.TIME):
		 */
		private Map<String, LocalDateTime> __currDateTimes;

		/**
		 * indicators & factors:
		 */
		private Table<String, String, Double> __currValues;

		/**
		 *
		 * @param headerNames headerNames
		 * @param constColumns constColumns
		 * @param constants constants
		 * @param recKeys recKeys
		 * @param indicatorNameMap indicatorNameMap
		 */
		public IndicatorsTableModel(List<String> headerNames, List<String> constColumns, Table<String, String, Object> constants,
		                            List<String> recKeys, Table<String, String, Format> indicatorNameMap) {

			__headerNames = headerNames;
			__constColumns = constColumns;
			__recKeys = recKeys;
			String seqCol = (String) __params.get(PARAM_SEQ_COLUMN);
			if (seqCol != null && seqCol.length() > 0 && headerNames.contains(seqCol)) {
				__seqColumn = Pair.of(seqCol, headerNames.indexOf(seqCol));
			} else {
				__seqColumn = null;
			}
			Map<String, String> m = new HashMap<>();
			for (Table.Cell<String, String, Format> c : indicatorNameMap.cellSet()) {
				m.put(c.getRowKey(), c.getColumnKey());
			}
			__indicatorNameBiMap = HashBiMap.create(m);
			__constants = constants;
			__currDateTimes = new HashMap<>();
			__currValues = null;
		}

		/**
		 *
		 * @param currData currData
		 */
		public void updateData(Pair<Map<String, LocalDateTime>, Table<String, String, Double>> currData) {

			synchronized (this) {
				__currDateTimes = currData.getLeft();
				__currValues = currData.getRight();
				fireTableDataChanged();
			}
		}

		@Override
		public int findColumn(String columnName) {

			return __headerNames.indexOf(columnName);
		}

		@Override
		public String getColumnName(int column) {

			return __headerNames.get(column);
		}

		@Override
		public int getRowCount() {

			return __recKeys.size();
		}

		@Override
		public int getColumnCount() {

			return __headerNames.size();
		}

		@Override
		public Object getValueAt(int rowIndex, int columnIndex) {

			try {  // try ... catch: 解决 jdk_11 莫名奇妙 NPE 问题:
				synchronized (this) {
					String recKey;
					try {
						recKey = __recKeys.get(rowIndex);
					} catch (IndexOutOfBoundsException e) {
						__log.debug("Row No. {} is NOT available!", rowIndex);
						return null;
					}
					// 排序列:
					if (__seqColumn != null && __seqColumn.getRight() == columnIndex) {
						return rowIndex + 1;
					}
					// 其他列:
					String colName = __headerNames.get(columnIndex);
					String indicatorName = __indicatorNameBiMap.inverse().get(colName);
					if (indicatorName == null) {
						indicatorName = colName;
					}
					// constants: WindCode, 名称, ...
					if (__constColumns.contains(colName)) {
						return __constants.get(recKey, colName);
					}
					// DateTime & indicators:
					if (Constants.DATE.equals(indicatorName)) {
						return !__currDateTimes.isEmpty() && __currDateTimes.get(recKey) != null ?
								__currDateTimes.get(recKey).toLocalDate() : null;
					} else if (Constants.TIME.equals(indicatorName)) {
						return !__currDateTimes.isEmpty() && __currDateTimes.get(recKey) != null ?
								__currDateTimes.get(recKey).toLocalTime() : null;
					} else {
						return __currValues != null ? __currValues.get(recKey, indicatorName) : Double.NaN;
					}
				}
			} catch (Exception ex) {
				__log.warn("Exception occured: {}", ExceptionUtils.getStackTrace(ex));
				return null;
			}
		}
	}

	/**
	 *
	 */
	private class IndicatorPullRunner implements Runnable {

		private final boolean __isTradingDay;

		public IndicatorPullRunner() {

			__isTradingDay = CommonDataUtils.isTradingDay(LocalDate.now());
		}

		@Override
		public void run() {

			__log.info("IndicatorPullRunner thread started.");

			long refreshInterval = ((int) __params.get(PARAM_REFRESH_INTERVAL)) * 1000;
			if (!__isDisposed && __isInit) {
				__pull();
			}
			while (!__isDisposed) {

				try {
					Thread.sleep(refreshInterval);
				} catch (InterruptedException e) {
					__log.warn("InterruptedException: {}", ExceptionUtils.getStackTrace(e));
				}

				LocalTime tm = LocalDateTime.now().toLocalTime();
				if (__isRunning) {
					if (__isTradingDay) {
						// 交易日, 若处于连续竞价期间以及边界时间, 则持续地进行定时刷新:
						if (tm.compareTo(Constants.OPEN_TIME_AM_CALL_AUCTION) >= 0
								&& tm.compareTo(Constants.CLOSE_TIME_AM) < 0
								|| tm.compareTo(Constants.OPEN_TIME_PM) >= 0
								&& tm.compareTo(Constants.CLOSE_TIME_PM_CALL_AUCTION) < 0) {
							__pull();
						}
					} else {
						// 非交易日, 只刷新一次, 然后结束刷新线程的运行:
						SwingPullView.this.stop();
					}
				}
			}

			__log.info("IndicatorPullRunner thread finished.");
		}
	}

	/**
	 * 记录的主键列 ColumnName, 例: WindCode:
	 */
	private static final String PARAM_REC_KEY_COL = "recKeyColumn";

	/**
	 * <pre>
	 * 展现的 指标名称 -> 列坐标 的映射; 例: {"RT_OPEN": "今开", "AAR_T1": "AAR_T1", ...};
	 * key: indicatorName;
	 * value: TableColumnName.
	 * </pre>
	 */
	private static final String PARAM_INDICS_NAME_MAP = "indicsNameMap";

	/**
	 * 排序列:
	 */
	public static final String PARAM_SORT_COLUMN = "sortColumn";

	/**
	 * 序号列:
	 */
	public static final String PARAM_SEQ_COLUMN = "seqColumn";

	/**
	 * 排序方式 (默认值: xlDescending (降序)):
	 */
	public static final String PARAM_SORT_ORDER = "sortOrder";

	/**
	 * 置顶行数 (默认为 5: 上证50, 沪深300, 中证500, 中证1000, 创业板综):
	 */
	public static final String PARAM_RESERVED_TOP_ROWS = "reservedTopRows";

	/**
	 * 刷新视图的时间间隔 (秒, 默认为 9 秒):
	 */
	public static final String PARAM_REFRESH_INTERVAL = "refreshInterval";

	/**
	 *
	 * @param forMap for BiMap
	 * @return BiMap
	 */
	protected static Map<String, String> forBiMap(Table<String, String, ?> forMap) {

		Map<String, String> m = new HashMap<>();
		for (Table.Cell<String, String, ?> c : forMap.cellSet()) {
			m.put(c.getRowKey(), c.getColumnKey());
		}
		return m;
	}

	/**
	 *
	 * @return (ViewMainForm, JFrame)
	 */
	protected static Pair<ViewMainForm, JFrame> buildViewMainForm(boolean showOrCreate) {

		ViewMainForm form = new ViewMainForm();
		JFrame frame = new JFrame("ViewMainForm");

		Thread frameThread = new Thread(new Runnable() {

			final Thread __activator = Thread.currentThread();
			@Override
			public void run() {

				frame.setContentPane(form.rootPanel);
				frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
				frame.pack();
				frame.setVisible(showOrCreate);
				try {
					__activator.join();
				} catch (InterruptedException e) {
					__log.warn("InterruptedException occurred: {}", ExceptionUtils.getStackTrace(e));
				}
			}
		});
		frameThread.start();

		return Pair.of(form, frame);
	}

	private final String __mode;

	private final Map<String, Object> __params;

	private final List<String> __allRecKeys;

	private final Inspectable __monitoring;

	private final Bar.Frequency __indicatorsSrcFreq;

	private ViewMainForm __mainForm;

	private JFrame __mainFrame;

	private Thread __iprThread;

	/**
	 * 是否已经初始化
	 */
	private boolean __isInit;

	/**
	 * 是否开启
	 */
	private boolean __isRunning;

	private boolean __isDisposed;

	/**
	 *
	 * @param monitoring monitoring
	 * @param srcFreq srcFreq
	 * @param showOnCreate showOnCreate
	 */
	public SwingPullView(Inspectable monitoring, Bar.Frequency srcFreq, boolean showOnCreate) {

		__mode = "PULL";
		__monitoring = monitoring;
		__indicatorsSrcFreq = srcFreq;
		__params = MapUtils.putAll(new HashMap<>(), new Object[][] {
				{ PARAM_REC_KEY_COL, "WindCode" }, // 记录的主键列的列坐标 (整列)
				{ PARAM_INDICS_NAME_MAP, HashBasedTable.create() }, // 展现的 指标名称 -> 列坐标 的映射; 例: {"RT_OPEN": "今开", "AAR_T1": "AAR_T1", ...}
				{ PARAM_SORT_COLUMN, "" }, // 排序列
				{ PARAM_SORT_ORDER, "xlDescending" }, // 排序方式 (默认值: xlDescending (2: 降序); xlAscending (1: 升序))
				{ PARAM_SEQ_COLUMN, "排序" }, // 序号列
				{ PARAM_RESERVED_TOP_ROWS, 5 }, // 置顶行数 (默认为 5: 上证50, 沪深300, 中证500, 中证1000, 创业板综)
				{ PARAM_REFRESH_INTERVAL, 9 } // 刷新视图的时间间隔 (秒, 默认为 9 秒)
		});
		__allRecKeys = new ArrayList<>();
		__isInit = false;
		__isRunning = false;
		__isDisposed = false;
		Pair<ViewMainForm, JFrame> p = buildViewMainForm(showOnCreate);
		__mainForm = p.getLeft();
		__mainFrame = p.getRight();
	}

	/**
	 *
	 * @param configCsvPath configCsvPath
	 * @param keyColName keyColName
	 * @param constColumns constColumns
	 * @param indicatorNameMap 展现的 指标名称 -> 列 的映射; 例: {"RT_OPEN": "今开", "AAR_T1": "AAR_T1", ...}
	 * @param paramsByName paramsByName
	 * @throws IOException IOException
	 */
	public void autoConfigure(Path configCsvPath, String keyColName, List<String> constColumns, Table<String, String, Format> indicatorNameMap,
	                          Map<String, Object> paramsByName) throws IOException {

		try (InputStream configCsvIn = new FileInputStream(configCsvPath.toFile())) {
			autoConfigure(configCsvIn, keyColName, constColumns, indicatorNameMap, paramsByName);
		}
	}

	/**
	 *
	 * @param configCsvIn configCsvIn
	 * @param keyColName keyColName
	 * @param constColumns constColumns
	 * @param indicatorNameMap 展现的 指标名称 -> 列 的映射; 例: {"RT_OPEN": "今开", "AAR_T1": "AAR_T1", ...}
	 * @param paramsByName paramsByName
	 * @throws IOException IOException
	 */
	public void autoConfigure(InputStream configCsvIn, String keyColName, List<String> constColumns, Table<String, String, Format> indicatorNameMap,
	                          Map<String, Object> paramsByName) throws IOException {

		__params.putAll(paramsByName);
		__params.put(PARAM_INDICS_NAME_MAP, indicatorNameMap);

		// config TableModels and Table columns:
		Pair<List<String>, List<CSVRecord>> configCsv = CommonUtils.readCsvSimple(configCsvIn);
		List<String> headerNames = configCsv.getLeft();
		JTable idxTable = __mainForm.getIndexesTable();
		JTable instrTable = __mainForm.getInstrumentsTable();
		BiMap<String, String> indicatorNameBiMap = HashBiMap.create(forBiMap(indicatorNameMap)).inverse();
		// init TableModel:
		List<CSVRecord> allConfigs = configCsv.getRight();
		// idxTable:
		int reservedTopRows = (Integer) __params.get(PARAM_RESERVED_TOP_ROWS);
		List<String> idxTableRecKeys = new ArrayList<>();
		Table<String, String, Object> idxTableConst = HashBasedTable.create(); // constants for idxTable
		for (int i = 0; i < reservedTopRows; i++) {
			CSVRecord rec = allConfigs.get(i);
			String recKey = rec.get(keyColName);
			idxTableRecKeys.add(recKey);
			for (String constCol : constColumns) {
				idxTableConst.put(recKey, constCol, rec.get(constCol));
			}
		}
		IndicatorsTableModel idxTableModel = new IndicatorsTableModel(headerNames, constColumns, idxTableConst, idxTableRecKeys,
				indicatorNameMap);
		idxTable.setModel(idxTableModel);
		TableRowSorter<IndicatorsTableModel> idxTableRs = new TableRowSorter<>(idxTableModel);
		// instrTable:
		List<String> instrTableRecKeys = new ArrayList<>();
		Table<String, String, Object> instrTableConst = HashBasedTable.create(); // constants for instrTable
		for (int i = reservedTopRows; i < allConfigs.size(); i++) {
			CSVRecord rec = allConfigs.get(i);
			String recKey = rec.get(keyColName);
			instrTableRecKeys.add(recKey);
			for (String constCol : constColumns) {
				instrTableConst.put(recKey, constCol, rec.get(constCol));
			}
		}
		IndicatorsTableModel instrTableModel = new IndicatorsTableModel(headerNames, constColumns, instrTableConst, instrTableRecKeys,
				indicatorNameMap);
		instrTable.setModel(instrTableModel);
		TableRowSorter<IndicatorsTableModel> instrTableRs = new TableRowSorter<>(instrTableModel);
		// init table columns:
		for (int i = 0; i < headerNames.size(); i++) {
			final Format fmt = indicatorNameMap.get(indicatorNameBiMap.get(headerNames.get(i)), headerNames.get(i));
			if (fmt instanceof NumberFormat) {
				idxTableRs.setComparator(i, Comparator.naturalOrder());
				instrTableRs.setComparator(i, Comparator.naturalOrder());
			} else {
				idxTableRs.setSortable(i, false);
				instrTableRs.setSortable(i, false);
			}
			// idxTable:
			TableColumn col = new TableColumn(i);
			col.setHeaderValue(headerNames.get(i));
			TableCellRenderer rd = new DefaultTableCellRenderer() {

				final Format format = fmt;

				@Override
				public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {

					Component c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
					if (value instanceof Double && !Double.isNaN((Double) value)) {
						if ((Double) value > 0) {
							c.setBackground(Color.RED);
						} else if ((Double) value < 0) {
							c.setBackground(Color.GREEN);
						}
					}
					return c;
				}

				@Override
				public void setValue(Object value) {

					if (format != null && value != null) {
						setText(format.format(value));
					} else {
						if (value == null) {
							setText("null");
						} else {
							super.setValue(value);
						}
					}
				}
			};
			col.setCellRenderer(rd);
			idxTable.addColumn(col);
			// instrTable:
			col = new TableColumn(i);
			col.setHeaderValue(headerNames.get(i));
			rd = new DefaultTableCellRenderer() {

				final Format format = fmt;

				@Override
				public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {

					Component c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
					if (value instanceof Double && !Double.isNaN((Double) value)) {
						if ((Double) value > 0) {
							c.setBackground(Color.RED);
						} else if ((Double) value < 0) {
							c.setBackground(Color.GREEN);
						}
					}
					return c;
				}

				@Override
				public void setValue(Object value) {

					if (format != null && value != null) {
						setText(format.format(value));
					} else {
						if (value == null) {
							setText("null");
						} else {
							super.setValue(value);
						}
					}
				}
			};
			col.setCellRenderer(rd);
			instrTable.addColumn(col);
		}
		idxTable.setRowSorter(idxTableRs);
		instrTable.setRowSorter(instrTableRs);

		__allRecKeys.addAll(idxTableRecKeys);
		__allRecKeys.addAll(instrTableRecKeys);

		// Refresh button:
		__mainForm.getRefreshButton().addActionListener(e -> __pull());
		// Start button:
		__mainForm.getStartButton().addActionListener(e -> start());
		// Stop button:
		__mainForm.getStopButton().addActionListener(e -> stop());

		__isInit = true;
	}

	/**
	 *
	 * @param key key
	 * @return value
	 */
	public Object getParam(String key) {

		return __params.get(key);
	}

	/**
	 *
	 */
	public void show() {

		__mainFrame.setVisible(true);
	}

	/**
	 *
	 */
	public void hide() {

		__mainFrame.setVisible(false);
	}

	/**
	 *
	 * @return __isRunning
	 */
	public boolean isRunning() {

		return __isRunning;
	}

	/**
	 *
	 * @return the ViewName
	 */
	public String getViewName() {

		// TODO:
		return "";
	}

	/**
	 *
	 * @return __mode
	 */
	public String getMode() {

		return __mode;
	}

	/**
	 *
	 * @return the DN
	 */
	public String getDistinguishedName(String workbook, String worksheet, String mode) {

		// TODO:
		return String.format("<%s, %s, %s>", workbook, worksheet, mode);
	}

	/**
	 *
	 */
	public void startRun(boolean autoStart) {

		__iprThread = new Thread(new IndicatorPullRunner());
		if (autoStart) {
			__isRunning = true;
		}
		__iprThread.start();
	}

	/**
	 *
	 */
	public void start() {

		if (__isInit) {
			__isRunning = true;
		}
		__log.info("SwingPullView started running.");
	}

	/**
	 *
	 */
	public void stop() {

		__isRunning = false;
		__log.info("SwingPullView stopped running.");
	}

	/**
	 *
	 */
	public void destroy() {

		__mainForm = null;
		if (__mainFrame != null) {
			__mainFrame.dispose();
		}
		__mainFrame = null;
		__isDisposed = true;
	}

	/**
	 *
	 * @return __allRecKeys
	 */
	public List<String> getKeyList() {

		return __allRecKeys;
	}

	/**
	 *
	 * @param indicatorName indicatorName
	 * @return indicatorName in __params[PARAM_INDICS_NAME_MAP]
	 */
	public boolean isRenderable(String indicatorName) {

		return ((Table<String, String, Format>)__params.get(PARAM_INDICS_NAME_MAP)).containsRow(indicatorName);
	}

	/**
	 *
	 * @param recKey row key
	 * @param indicatorName export by Strategy
	 * @param value value
	 */
	public void render(String recKey, String indicatorName, Object value) {

	}

	/**
	 *
	 */
	public void refresh() {

		__pull();
	}

	private void __pull() {

		if (__monitoring != null) {
			List<String> indicators = new ArrayList<>(((Table) __params.get(PARAM_INDICS_NAME_MAP)).rowKeySet());
			Pair<Map<String, LocalDateTime>, Table<String, String, Double>> newData = __monitoring.checkNow(__indicatorsSrcFreq,
					Optional.empty(), Optional.of(indicators));
			if (newData != null) {
				((IndicatorsTableModel) __mainForm.getIndexesTable().getModel()).updateData(newData);
				((IndicatorsTableModel) __mainForm.getInstrumentsTable().getModel()).updateData(newData);
			}
		}
	}
}
